#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/select.h>
#include <memory>
#include <algorithm>
#include "Socket.hpp"
#include "Log.hpp"

using namespace SocketModule;
using namespace LogModule;

class SelectServer
{
    const static int size = sizeof(fd_set) * 8;
    const static int defaultfd = -1;

public:
    SelectServer(int port)
        : _listensock(std::make_unique<TcpSocket>()),
          _isrunning(false)
    {
        // 完成套接字初始化操作
        _listensock->BuildTcpSocketMethod(port);

        // 初始化辅助数组
        for (int i = 0; i < size; i++)
        {
            _fd_array[i] = defaultfd;
        }

        // 先把我们的listen套接字记录到第一个位置
        _fd_array[0] = _listensock->Fd();
    }

    // 启动服务器
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // auto res=_listensock->Accept(); 这样是不行的
            // 因为listensockfd也是一个fd，进程怎么知道listenfd上面有新连接到来了呢？
            // 答：是通过listensockfd进行io读取到的
            // 所以我们在这里得将其添加到select内部
            // 让操作系统添加到select内部，让os帮我们关心listensockfd上面的读事件

            fd_set fds;    // 定义fds集合
            FD_ZERO(&fds); // 清空fds
            // 用来记录辅助数组中有的最大fd，将来是要+1成为select第一个参数的
            int maxfd = defaultfd;
            // FD_SET(_listensock->Fd(), &fds); // 将listen套接字设置进fds集合中
            // 这里引入了辅助数组来记录当前服务器上所有的fd，那么我们需要把这些fd都设置到fds中
            for (int i = 0; i < size; i++)
            {
                if (_fd_array[i] == defaultfd) // 如果还是默认的fd，就说明该位置没有合法放到
                    continue;
                // 到这说明是要被设置到fds集合中的fd，第一个位置就是listenfd
                // 1.每次select之前都要对rfds进行重置
                FD_SET(_fd_array[i], &fds);
                // 2.更新出数组中最大fd值
                maxfd = max(maxfd, _fd_array[i]);
            }
            PrintFd();

            // 这里还没有把listen套接字设置进内核中，因为在这个方法中是在用户栈上的
            // 还需要调用select来设置到内核
            // struct timeval timeout = {2, 0}; // 可以设置一下select时间
            // 也可以传nullptr表示阻塞等待

            // select返回之后，我们怎么知道哪些fd需要被添加到rfds，让select关心呢？
            // 所以select要进行完整的设计，需要借助一个辅助数组
            // 用来保存服务器历史获取过的所有fd

            // 最大fd，也就是第一个参数+1前的fd一定是变化的，我们传上面的maxfd
            // 每次select之前都要对rfds进行重置
            int n = select(maxfd + 1, &fds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case -1:
                // select出错了
                LOG(LogLevel::ERROR) << "select error";
                break;
            case 0:
                // 表示超时了
                LOG(LogLevel::INFO) << "time out...";
                break;
            default:
                // 到这说明有事件就绪了
                // 这个事件不仅仅指有新链接到来了，有可能是读事件就绪
                LOG(LogLevel::DEBUG) << "有事件就绪了...";
                Dispatcher(fds); // 此时就可以处理就绪的事件，使用Dispatcher任务派发器
                break;
            }
        }
        _isrunning = false;
    }

    // Dispatcher任务派发器
    void Dispatcher(fd_set &fds)
    {
        // 这个事件不仅仅指有新链接到来了，也有可能是读事件就绪
        // 所以在这里得进行判断，指定的fd在被修改之后的fds中就证明该fd读事件就绪了
        for (int i = 0; i < size; i++)
        {
            if (_fd_array[i] == defaultfd)
            {
                continue;
            }
            // fd合法，于是进行判断是否就绪，也就是该fd是否在fds中，使用FD_ISSET函数
            if (FD_ISSET(_fd_array[i], &fds))
            {
                // 到这说明这个fd读事件就绪了
                // 可现在有个问题，对于listensockfd，新连接到来时也是读事件就绪呀
                // 普通的sockfd数据到来，也是读事件就绪，要怎么进行区分呢？
                // 很简单，我们判断一下就好啦
                if (_fd_array[i] == _listensock->Fd())
                {
                    // listensockfd 有新连接到来
                    Accepter(); // 那就执行获取新连接的方法
                }
                else
                {
                    // sockfd 有数据到来
                    Recver(_fd_array[i], i); // 那就执行读取数据的方法
                }
            }
        }
    }

    // 获取新连接的方法
    // 也叫连接管理器
    void Accepter()
    {
        InetAddr client;
        // 此时是在读事件就绪时也就是有连接时才调用的accept，所以此时不会发生阻塞，直接读取
        // 这就是多路转接
        int sockfd = _listensock->Accept(&client);
        if (sockfd >= 0)
        {
            // 获取新连接到来成功，然后我们可以直接read/recv()不？
            // 答案是当然不行，因为我们的sockfd是否读事件就绪是不清楚的
            // 贸然调用读取接口不还是会出现阻塞的情形咩
            // 只有select最清楚未来sockfd上是否有事件就绪

            // 所以在这里得先将新的sockfd托管给select
            LOG(LogLevel::DEBUG) << "get a new link,sockfd: " << sockfd
                                 << "client is: " << client.StringAddr();
            // 如何托管？答：将新的fd放入辅助数组中
            int pos = 0;
            for (; pos < size; pos++)
            {
                if (_fd_array[pos] == defaultfd)
                    break; // 表示该位置可以被设置
            }
            // 出来判断一下pos的大小
            if (pos == size)
            {
                // 说明没有位置可以放新来的fd了，也就是服务器满了
                LOG(LogLevel::WARNING) << "select server full";
                close(sockfd); // 那么我们只能结束掉这个连接了
            }
            else
            {
                // 找到了合法的位置
                _fd_array[pos] = sockfd;
            }
        }
    }

    // 读取数据的方法
    void Recver(int fd, int pos)
    {
        // 到这的时候select等待完成是确保这个fd中有数据可以读取的，读事件是就绪的
        // 所以在这里调用read/recv进行读取操作时并不会阻塞
        char buffer[1024];
        // 注意：这里的recv由于是tcp协议，所以还是会有老问题的——可能读取的数据不完整！
        ssize_t n = recv(fd, buffer, sizeof(buffer), 0);
        if (n > 0)
        {
            // 读取成功
            buffer[n] = 0;
            std::cout << "client say: " << buffer << std::endl;
        }
        else if (n == 0)
        {
            // 表示客户端退出了
            LOG(LogLevel::INFO) << "client quit...";
            // 那么这个fd在被要关掉的同时，还应该把它从辅助数组中删去
            // 表示select不需要关心它的读事件了，也就不需要在辅助数组中了
            _fd_array[pos] = defaultfd; // 将该位置置为默认就好
            close(fd);
        }
        else
        {
            // 读取失败
            LOG(LogLevel::ERROR) << "recv error";
        }
    }

    void PrintFd()
    {
        std::cout << "_fd_array[]: ";
        for (int i = 0; i < size; i++)
        {
            if (_fd_array[i] == defaultfd)
            {
                continue;
            }
            std::cout << _fd_array[i] << " ";
        }
        std::cout << "\r\n";
    }

    // 停止服务器
    void Stop()
    {
        _isrunning = false;
    }

    ~SelectServer() {}

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    int _fd_array[size];
};